#include "chatserver.h"
#include <QElapsedTimer>
#include <QLogger>
#include <QRegExp>
#include <QTcpSocket>
#include <QtDebug>
#include <iostream>

using namespace chatserver;

const int chatserver::TcpClient::WaitIDTimeout = 3000; //[ms]

ChatServer::ChatServer(int port, QHostAddress addr, QObject *parent)
    : QTcpServer(parent),
      logger(new QLogger("ChatServer", QLogger::Console, this)) {
  qRegisterMetaType<chatserver::MesgMeta>("chatserver::MesgMeta");
  // tencent server require Intranet IP
  //    if(this->listen(QHostAddress("172.17.0.11"),9999))
  if (this->listen(addr, port))
    logger->info("ChatServer listen on " + addr.toString() + ":" +
                 QString::number(port));
}
ChatServer::~ChatServer() {}

void ChatServer::incomingConnection(qintptr socketDescriptor) {
  TcpClient *client = new TcpClient(logger, socketDescriptor);

  QThread *thread = new QThread;
  client->moveToThread(thread);
  connect(thread, &QThread::started, client, &TcpClient::init);
  connect(client, &QObject::destroyed, thread, &QThread::quit);
  connect(thread, &QThread::finished, thread, &QObject::deleteLater);
  connect(client, &TcpClient::connectTo, this, &ChatServer::addNewClient,
          Qt::BlockingQueuedConnection);

  thread->start();
}

void ChatServer::addNewClient(const QString &id) {
  chatserver::TcpClient *client = dynamic_cast<TcpClient *>(QObject::sender());
  if (map.contains(id)) {
    logger->error("Cannot add client [" + id + "]");
    client->deleteLater();
    return;
  }
  disconnect(client, &TcpClient::connectTo, this, &ChatServer::addNewClient);
  connect(client, &TcpClient::sendTo, this, &ChatServer::toSendSlot);
  connect(client, &TcpClient::setGrp, this, &ChatServer::changeClientGroup);
  connect(client, &TcpClient::disconnected, this, &ChatServer::removeClient,
          Qt::BlockingQueuedConnection);

  map.insert(id, client);
  logger->info("Add Client [" + id + "]");
}

void ChatServer::removeClient(const QString &id, const QString &group) {
  logger->info("Remove Client [" + id + "]");
  map[id]->deleteLater();
  map.remove(id);
  if (groupMap.contains(group)) {
    groupMap[group].remove(id);
  }
}

void ChatServer::changeClientGroup(const QString &id, const QString &oldGrp,
                                   const QString &newGrp) {
  if (!oldGrp.isEmpty()) {
    groupMap[oldGrp].remove(id);
    if (groupMap[oldGrp].size()) {
      groupMap.remove(oldGrp);
    }
  }
  groupMap[newGrp].insert(id);
}

void ChatServer::toSendSlot(const QByteArray &data,
                            const chatserver::MesgMeta &meta) {
  using namespace chatserver;
  if (meta.disT == Individual) {
    if (!map.contains(meta.id)) {
      logger->error("Cannot send to [" + meta.id + "] from [" + meta.sender +
                    "]");
      return;
    }
    QMetaObject::invokeMethod(
        map[meta.id], meta.contentT == Text ? "sendMessage" : "sendCommand",
        Qt::QueuedConnection, Q_ARG(QByteArray, data),
        Q_ARG(QString, meta.sender));
  } else {
    switch (meta.disT) {
    case Individual:
      Q_ASSERT(0);
    case Group: {
      if (!groupMap.contains(meta.id)) {
        logger->error("Cannot send to group {" + meta.id + "} from [" +
                      meta.sender + "]");
        break;
      }
      foreach (auto recv, groupMap[meta.id]) {
        QMetaObject::invokeMethod(
            map[recv], meta.contentT == Text ? "sendMessage" : "sendCommand",
            Qt::QueuedConnection, Q_ARG(QByteArray, data),
            Q_ARG(QString, meta.sender));
      }
      return;
    } break;
    case Broadcast: {
      for (auto it = map.begin(); it != map.end(); ++it) {
        if (it.key() == meta.sender)
          continue;
        QMetaObject::invokeMethod(
            it.value(), meta.contentT == Text ? "sendMessage" : "sendCommand",
            Qt::QueuedConnection, Q_ARG(QByteArray, data),
            Q_ARG(QString, meta.sender));
      }
    } break;
    }
  }
}

TcpClient::TcpClient(QLogger *logger, qintptr sd)
    : QObject(), logger(logger), socketD(sd) {}
TcpClient::~TcpClient() {}

void TcpClient::init() {
  Socket = new QTcpSocket();
  Socket->setSocketDescriptor(socketD);
  QElapsedTimer timer;
  int leftTime = WaitIDTimeout;
  timer.start();
  while (Socket->waitForReadyRead(leftTime) && leftTime >= 0) {
    pack << Socket->readAll();
    if (pack) {
      QByteArray idba;
      pack >> idba;
      ID = QString::fromUtf8(idba);
      emit connectTo(ID, this); // there is a blocking call
      recvRawMesg();            // deal with the left byte (if has)
      connect(Socket, &QTcpSocket::readyRead, this, &TcpClient::recvRawMesg);
      connect(Socket, &QTcpSocket::disconnected, this, &TcpClient::close);
      return;
    } else {
      leftTime -= timer.elapsed();
    }
  }
  logger->error("Confirm ID timeout");
  close();
}
void TcpClient::close() {
  emit disconnected(ID, BelongGroup);
  Socket->deleteLater();
  this->deleteLater();
}

void TcpClient::sendMessage(const QByteArray &meg, const QString &sender) {
  Socket->write(ChatDataPack::pack("$text " + sender.toUtf8() + "$" + meg));
}

void TcpClient::sendCommand(const QByteArray &cmd, const QString &sender) {
  Socket->write(ChatDataPack::pack("$cmd " + sender.toUtf8() + "$" + cmd));
}

void TcpClient::recvRawMesg() {
  pack << Socket->readAll();
  while (pack) {
    QByteArray out;
    pack >> out;
    parse(out);
  }
}

void TcpClient::parse(const QByteArray &mesg) {
  /**************************************************/
  // parse
  QRegExp rx("^(\\$[^\\$]*\\$)?(.*)");
  rx.exactMatch(mesg);
  QStringList list = rx.capturedTexts();
  QString commandPart = list.at(1);
  QString userInfo = list.at(2);
  //  qDebug() << list;
  if (commandPart.isEmpty()) {
    //    // No command
    //    if (!isTarget) {
    //      Warning("No setTarget");
    //    } else {
    //      emit sendTo(targetID, userInfo.toUtf8(), ID);
    //      Info("SEND TO " + targetID);
    //    }
    QLogger::Error("Command Part empty in parse()");
  } else {
    // has command
    chatserver::MesgMeta meta;
    meta.sender = ID;
    commandPart.remove(0, 1);
    commandPart.remove(commandPart.length() - 1, 1);
    auto commandList = commandPart.split(" ");
    auto command = commandList.at(0);
    if (command == "sendTo") {
      if (commandList.length() != 2) {
        Warning("SENDTO COMMAND need target");
      } else {
        meta.id = commandList.at(1).toUtf8();
        meta.disT = chatserver::Individual;
        meta.contentT = chatserver::Text;
        emit sendTo(userInfo.toUtf8(), meta);
        Info("SEND TO " + commandList.at(1));
      }
    } else if (command == "setTarget") {
      if (commandList.length() != 2) {
        Warning("SETTARGET COMMAND need target");
      } else {
        targetID = commandList.at(1).toUtf8();
        isTarget = true;
        Info("SETTARGET " + commandList.at(1));
      }
    } else if (command == "unsetTarget") {
      targetID.clear();
      isTarget = false;
      Info("CLEARTARGET");
    } else if (command == "setGroup") {
      QString preGrp = BelongGroup;
      BelongGroup = commandList.at(1);
      emit setGrp(ID, preGrp, BelongGroup);
      Info("SETGROUP " + BelongGroup);
    } else if (command == "sendToGroup") {
      meta.id = commandList.at(1).toUtf8();
      meta.disT = chatserver::Group;
      meta.contentT = chatserver::Text;
      emit sendTo(userInfo.toUtf8(), meta);
      Info("SEND to Group {" + commandList.at(1) + "}: " + userInfo.toUtf8());
    } else if (command == "unsetGroup") {
      QString preGrp = BelongGroup;
      BelongGroup.clear();
      emit setGrp(ID, preGrp, BelongGroup);
      Info("RESET Group");
    } else if (command == "broadcast") {
      meta.id = commandList.at(1).toUtf8();
      meta.disT = chatserver::Broadcast;
      meta.contentT = chatserver::Text;
      emit sendTo(userInfo.toUtf8(), meta);
      Info("BROADCAST " + userInfo.toUtf8());
    } else if (command == "sendCmdTo") {
      meta.id = commandList.at(1).toUtf8();
      meta.disT = chatserver::Individual;
      meta.contentT = chatserver::Command;
      emit sendTo(userInfo.toUtf8(), meta);
      Info("SEND COMMAND TO " + commandList.at(1));
    } else if (command == "sendCmdToGrp") {
      meta.id = commandList.at(1).toUtf8();
      meta.disT = chatserver::Group;
      meta.contentT = chatserver::Command;
      emit sendTo(userInfo.toUtf8(), meta);
      Info("SEND COMMAND to Group {" + commandList.at(1) + "}");
    } else {
      Warning("CANNOT parse cammond");
    }
  }
}

QString TcpClient::printPrefix() { return "TCP Client " + ID + "::"; }

void TcpClient::Info(const QString &info) {
  QLogger::Info(printPrefix() + "INFO::" + info);
}

void TcpClient::Warning(const QString &info) {
  QString warning = printPrefix() + "WARNING::" + info;
  QLogger::Warn(warning);
  chatserver::MesgMeta meta;
  meta.sender = ID;
  meta.id = ID;
  meta.disT = chatserver::Individual;
  meta.contentT = chatserver::Text;
  emit sendTo(warning.toUtf8(), meta);
}
